Custom Feeds
Custom feeds, or feed generators, are services that provide custom algorithms to users through the AT Protocol. This allows users to choose their own timelines, whether it's an algorithmic For You page or a feed of entirely cat photos.
This is a starter kit for creating feed generators on atproto. It's not feature complete, but should give you a good starting ground off of which to build and deploy a feed.
You can skip this tutorial and clone the TypeScript starter template directly here, or use community templates in Python or Ruby.
Overview
Custom feeds work very simply: the server receives a request from a user's server and returns a list of post URIs with some optional metadata attached. Those posts are then hydrated into full views by the requesting server and sent back to the client.
A Feed Generator service can host one or more algorithms. The service itself is identified by DID, while each algorithm that it hosts is declared by a record in the repo of the account that created it. For instance, feeds offered by Bluesky will likely be declared in @bsky.app
's repo. Therefore, a given algorithm is identified by the at-uri of the declaration record. This declaration record includes a pointer to the service's DID along with some profile information for the feed.
The general flow of providing a custom algorithm to a user is as follows:
- A user requests a feed from their server (PDS) using the at-uri of the declared feed
- The PDS resolves the at-uri and finds the DID doc of the Feed Generator
- The PDS sends a
getFeedSkeleton
request to the service endpoint declared in the Feed Generator's DID doc- This request is authenticated by a JWT signed by the user's repo signing key
- The Feed Generator returns a skeleton of the feed to the user's PDS
- The PDS hydrates the feed (user info, post contents, aggregates, etc.)
- In the future, the PDS will hydrate the feed with the help of an App View, but for now, the PDS handles hydration itself
- The PDS returns the hydrated feed to the user
For users, this should feel like visiting a page in the app. Once they subscribe to a custom algorithm, it will appear in their home interface as one of their available feeds.
Starter Template
Getting Started
Clone the starter kit here. We've set up this simple server with SQLite to store and query data. Feel free to switch this out for whichever database you prefer.
Next, you will need to do two things:
-
Implement indexing logic in
src/subscription.ts
.This will subscribe to the repo subscription stream on startup, parse events and index them according to your provided logic.
-
Implement feed generation logic in
src/algos
.For inspiration, we've provided a very simple feed algorithm (
whats-alf
) that returns all posts related to the titular character of the TV show ALF.You can either edit it or add another algorithm alongside it. The types are in place, and you will just need to return something that satisfies the
SkeletonFeedPost[]
type.
We've taken care of setting this server up with a did:web. However, you're free to switch this out for did:plc if you like - you may want to if you expect this Feed Generator to be long-standing and possibly migrating domains.
Deploying your feed
Your feed will need to be accessible at the value supplied to the FEEDGEN_HOSTNAME
environment variable.
The service must be set up to respond to HTTPS queries over port 443.
Publishing your feed
To publish your feed, go to the script at scripts/publishFeedGen.ts
and fill in the variables at the top. Examples are included, and some are optional. To publish your feed generator, simply run yarn publishFeed
.
To update your feed's display data (name, avatar, description, etc.), just update the relevant variables and re-run the script.
After successfully running the script, you should be able to see your feed from within the app, as well as share it by embedding a link in a post (similar to a quote post).
Running the Server
Install dependencies with yarn
and then run the server with yarn start
. This will start the server on port 3000, or what's defined in .env
. You can then watch the firehose output in the console and access the output of the default custom ALF feed at http://localhost:3000/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:example:alice/app.bsky.feed.generator/whats-alf.
Some Details
Pagination
You'll notice that the getFeedSkeleton
method returns a cursor
in its response and takes a cursor
param as input.
This cursor is treated as an opaque value and fully at the Feed Generator's discretion. It is simply passed through the PDS directly to and from the client.
We strongly encourage that the cursor be unique per feed item to prevent unexpected behavior in pagination.
We recommend, for instance, a compound cursor with a timestamp + a CID:
1683654690921::bafyreia3tbsfxe3cc75xrxyyn6qc42oupi73fxiox76prlyi5bpx7hr72u
Language handling
When making requests to getFeedSkeleton
, clients are encouraged to populate the Accept-Language
HTTP header with comma-separated BCP-47 language codes e.g. en,pr-BR
. Feed generators can use this language context to filter or rank posts. If language filtering is applied, the feed generator should use the the Content-Language
response header indicating the parsed language codes.
Suggestions and Examples
How a feed generator fulfills the getFeedSkeleton
request is completely at their discretion. At the simplest end, a Feed Generator could supply a "feed" that only contains some hardcoded posts.
For most use cases, we recommend subscribing to the firehose at com.atproto.sync.subscribeRepos
. This websocket will send you every record that is published on the network. Since Feed Generators do not need to provide hydrated posts, you can index as much or as little of the firehose as necessary.
Depending on your algorithm, you likely do not need to keep posts around for long. Unless your algorithm is intended to provide "posts you missed" or something similar, you can likely garbage collect any data that is older than 48 hours.
Some examples:
- A community feed: Compile a list of DIDs within that community and filtering the firehose for all posts from users within that list.
- A topical feed: Filter the algorithm for posts and pass the post text through some filtering mechanism (an LLM, a keyword matcher, etc.) that filters for the topic of your choice.